<html>

<head>
    <meta charset="utf8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes">
    <title>WebGL 3D Lathe Compute Normals</title>
    <link type="text/css" href="../resources/webgl-tutorials.css" rel="stylesheet" />
</head>

<body>
    <div id="info">
        <a href="https://webglfundamentals.org/webgl/lessons/zh_cn/webgl-3d-geometry-lathe.html" target="_blank">WebGL
            三维几何加工</a>
    </div>
    <canvas></canvas>
    <div id="uiContainer">
        <div id="ui">
        </div>
    </div>
</body>
<!-- vertex shader -->
<script id="simple-vertex-shader" type="notjs">
attribute vec4 a_position;
attribute vec2 a_texcoord;
attribute vec3 a_normal;

uniform mat4 u_matrix;

varying vec2 v_texcoord;
varying vec3 v_normal;

void main() {
  // Multiply the position by the matrix.
  gl_Position = u_matrix * a_position;

  // Pass the texcoord to the fragment shader.
  v_texcoord = a_texcoord;
  v_normal = a_normal;
}
</script>
<script id="specular-vertex-shader" type="notjs">
attribute vec4 a_position;
attribute vec3 a_normal;

uniform vec3 u_lightWorldPosition;
uniform vec3 u_viewWorldPosition;

uniform mat4 u_world;
uniform mat4 u_worldViewProjection;
uniform mat4 u_worldInverseTranspose;

varying vec3 v_normal;
varying vec3 v_surfaceToLight;
varying vec3 v_surfaceToView;

void main() {
  // Multiply the position by the matrix.
  gl_Position = u_worldViewProjection * a_position;

  // orient the normals and pass to the fragment shader
  v_normal = mat3(u_worldInverseTranspose) * a_normal;

  // compute the world position of the surfoace
  vec3 surfaceWorldPosition = (u_world * a_position).xyz;

  // compute the vector of the surface to the light
  // and pass it to the fragment shader
  v_surfaceToLight = u_lightWorldPosition - surfaceWorldPosition;

  // compute the vector of the surface to the view/camera
  // and pass it to the fragment shader
  v_surfaceToView = u_viewWorldPosition - surfaceWorldPosition;
}
</script>
<!-- fragment shader -->
<script id="normal-shader" type="notjs">
precision mediump float;

// Passed in from the vertex shader.
varying vec2 v_texcoord;
varying vec3 v_normal;

uniform sampler2D u_texture;

void main() {
  gl_FragColor = vec4(v_normal * .5 + .5, 1);
}
</script>
<script id="texture-shader" type="notjs">
precision mediump float;

// Passed in from the vertex shader.
varying vec2 v_texcoord;
varying vec3 v_normal;

uniform sampler2D u_texture;

void main() {
  gl_FragColor = texture2D(u_texture, v_texcoord);
}
</script>
<script id="specular-shader" type="notjs">
#if GL_FRAGMENT_PRECISION_HIGH
  precision highp float;
#else
  precision mediump float;
#endif

// Passed in from the vertex shader.
varying vec3 v_normal;
varying vec3 v_surfaceToLight;
varying vec3 v_surfaceToView;

uniform vec4 u_color;
uniform float u_shininess;

void main() {
  // because v_normal is a varying it's interpolated
  // we it will not be a uint vector. Normalizing it
  // will make it a unit vector again
  vec3 normal = normalize(v_normal);
  vec3 surfaceToLightDirection = normalize(v_surfaceToLight);
  vec3 surfaceToViewDirection = normalize(v_surfaceToView);
  vec3 halfVector = normalize(surfaceToLightDirection + surfaceToViewDirection);

  float light = dot(normal, surfaceToLightDirection);
  float specular = 0.0;
  if (light > 0.0) {
    specular = pow(dot(normal, halfVector), u_shininess);
  }

  gl_FragColor = u_color;

  // Lets multiply just the color portion (not the alpha)
  // by the light
  gl_FragColor.rgb *= light;

  // Just add in the specular
  gl_FragColor.rgb += specular;
}
</script>
<!-- this is included only for iframe and requestAnimationFrame support -->
<!--
for most samples webgl-utils only provides shader compiling/linking and
canvas resizing because why clutter the examples with code that's the same in every sample.
See http://webglfundamentals.org/webgl/lessons/webgl-boilerplate.html
and http://webglfundamentals.org/webgl/lessons/webgl-resizing-the-canvas.html
for webgl-utils, m3, m4, and webgl-lessons-ui.
-->
<script src="../resources/webgl-lessons-ui.js"></script>
<script src="../resources/webgl-utils.js"></script>
<script src="../resources/3d-math.js"></script>
<script src="../resources/2d-math.js"></script>
<script>
    "use strict";

    //const svg = "m44,434c18,-33 19,-66 15,-111c-4,-45 -37,-104 -39,-132c-2,-28 11,-51 16,-81c5,-30 3,-63 -36,-63";
    const svg =
        "M236,124L197,112L197,34C197,34 184.859,31.871 165,33C186.997,66.892 161.894,89.627 173,109C184.106,128.373 186.493,137.68 205,144C219.37,148.907 222,154 222,154L220,175L202,174L191,194L204,209L222,208C222,208 226.476,278.566 218,295C209.524,311.434 191.013,324.945 201,354C210.987,383.055 213,399 213,399L191,403L191,417L212,422C212,422 233.283,437.511 211,444C188.717,450.489 111,472 111,472L111,485L236,484L236,124Z";

    function main() {
        const opt = getQueryParams();
        const curvePoints = parseSVGPath(svg, { xFlip: true });

        // Get A WebGL context
        /** @type {HTMLCanvasElement} */
        const canvas = document.querySelector("canvas");
        const gl = canvas.getContext("webgl");
        if (!gl) {
            return;
        }

        const data = {
            tolerance: 0.15,
            distance: .4,
            divisions: 16,
            startAngle: 0,
            endAngle: Math.PI * 2,
            capStart: true,
            capEnd: true,
            triangles: true,
            maxAngle: degToRad(30),
            mode: 0,
        };

        function generateMesh(bufferInfo) {
            const tempPoints = getPointsOnBezierCurves(curvePoints, data.tolerance);
            const points = simplifyPoints(tempPoints, 0, tempPoints.length, data.distance);
            const tempArrays = lathePoints(points, data.startAngle, data.endAngle, data.divisions, data.capStart, data.capEnd);
            const arrays = generateNormals(tempArrays, data.maxAngle);
            const extents = getExtents(arrays.position);
            if (!bufferInfo) {
                // calls gl.createBuffer, gl.bindBuffer, and gl.bufferData for each array
                bufferInfo = webglUtils.createBufferInfoFromArrays(gl, arrays);
            } else {
                gl.bindBuffer(gl.ARRAY_BUFFER, bufferInfo.attribs.a_position.buffer);
                gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(arrays.position), gl.STATIC_DRAW);
                gl.bindBuffer(gl.ARRAY_BUFFER, bufferInfo.attribs.a_texcoord.buffer);
                gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(arrays.texcoord), gl.STATIC_DRAW);
                gl.bindBuffer(gl.ARRAY_BUFFER, bufferInfo.attribs.a_normal.buffer);
                gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(arrays.normal), gl.STATIC_DRAW);
                gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, bufferInfo.indices);
                gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(arrays.indices), gl.STATIC_DRAW);
                bufferInfo.numElements = arrays.indices.length;
            }
            return {
                bufferInfo: bufferInfo,
                extents: extents,
            };
        }

        // used to force the locations of attributes so they are
        // the same across programs
        const attributes = ['a_position', 'a_texcoord', 'a_normal'];
        // setup GLSL program
        const programInfos = [
            // compiles shaders, links program and looks up locations
            webglUtils.createProgramInfo(gl, ["simple-vertex-shader", "normal-shader"], attributes),
            webglUtils.createProgramInfo(gl, ["specular-vertex-shader", "specular-shader"], attributes),
            webglUtils.createProgramInfo(gl, ["simple-vertex-shader", "texture-shader"], attributes),
        ];

        const texInfo = loadImageAndCreateTextureInfo("../resources/uv-grid.png", render);

        let worldMatrix = m4.identity();
        let projectionMatrix;
        let extents;
        let bufferInfo;

        function update() {
            const info = generateMesh(bufferInfo);
            extents = info.extents;
            bufferInfo = info.bufferInfo;
            render();
        }
        update();

        function render() {
            webglUtils.resizeCanvasToDisplaySize(gl.canvas, window.devicePixelRatio);

            // Tell WebGL how to convert from clip space to pixels
            gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);

            gl.enable(gl.DEPTH_TEST);

            // Clear the canvas AND the depth buffer.
            gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);

            // Compute the projection matrix
            const fieldOfViewRadians = Math.PI * .25;
            const aspect = gl.canvas.clientWidth / gl.canvas.clientHeight;
            projectionMatrix =
                m4.perspective(fieldOfViewRadians, aspect, 1, 2000);

            // Compute the camera's matrix using look at.
            const midY = lerp(extents.min[1], extents.max[1], .5);
            const sizeToFitOnScreen = (extents.max[1] - extents.min[1]) * .6;
            const distance = sizeToFitOnScreen / Math.tan(fieldOfViewRadians * .5);
            const cameraPosition = [0, midY, distance];
            const target = [0, midY, 0];
            const up = [0, -1, 0];  // we used 2d points as input which means orientation is flipped
            const cameraMatrix = m4.lookAt(cameraPosition, target, up);

            // Make a view matrix from the camera matrix.
            const viewMatrix = m4.inverse(cameraMatrix);

            const viewProjectionMatrix = m4.multiply(projectionMatrix, viewMatrix);

            const programInfo = programInfos[data.mode];
            gl.useProgram(programInfo.program);

            // Setup all the needed attributes.
            // calls gl.bindBuffer, gl.enableVertexAttribArray, gl.vertexAttribPointer for each attribute
            webglUtils.setBuffersAndAttributes(gl, programInfo, bufferInfo);

            const worldViewProjection = m4.multiply(viewProjectionMatrix, worldMatrix);

            // Set the uniforms
            // calls gl.uniformXXX, gl.activeTexture, gl.bindTexture
            webglUtils.setUniforms(programInfo, {
                u_matrix: worldViewProjection,
                u_worldViewProjection: worldViewProjection,
                u_world: worldMatrix,
                u_worldInverseTranspose: m4.transpose(m4.inverse(worldMatrix)),
                u_lightWorldPosition: [midY * 1.5, midY * 2, distance * 1.5],
                u_viewWorldPosition: cameraMatrix.slice(12, 15),
                u_color: [1, 0.8, 0.2, 1],
                u_shininess: 50,
                u_texture: texInfo.texture,
            });

            // calls gl.drawArrays or gl.drawElements.
            webglUtils.drawBufferInfo(gl, bufferInfo, data.triangles ? gl.TRIANGLE : gl.LINES);
        }

        function getExtents(positions) {
            const min = positions.slice(0, 3);
            const max = positions.slice(0, 3);
            for (let i = 3; i < positions.length; i += 3) {
                min[0] = Math.min(positions[i + 0], min[0]);
                min[1] = Math.min(positions[i + 1], min[1]);
                min[2] = Math.min(positions[i + 2], min[2]);
                max[0] = Math.max(positions[i + 0], max[0]);
                max[1] = Math.max(positions[i + 1], max[1]);
                max[2] = Math.max(positions[i + 2], max[2]);
            }
            return {
                min: min,
                max: max,
            };
        }

        // get the points from an SVG path. assumes a continous line
        function parseSVGPath(svg, opt) {
            const points = [];
            let delta = false;
            let keepNext = false;
            let need = 0;
            let value = '';
            let values = [];
            let lastValues = [0, 0];
            let nextLastValues = [0, 0];
            let mode;

            function addValue() {
                if (value.length > 0) {
                    const v = parseFloat(value);
                    if (v > 1000) {
                        debugger;  // eslint-disable-line
                    }
                    values.push(v);
                    if (values.length === 2) {
                        if (delta) {
                            values[0] += lastValues[0];
                            values[1] += lastValues[1];
                        }
                        points.push(values);
                        if (keepNext) {
                            nextLastValues = values.slice();
                        }
                        --need;
                        if (!need) {
                            if (mode === 'l') {
                                const m4 = points.pop();
                                const m1 = points.pop();
                                const m2 = v2.lerp(m1, m4, 0.25);
                                const m3 = v2.lerp(m1, m4, 0.75);
                                points.push(m1, m2, m3, m4);
                            }
                            lastValues = nextLastValues;
                        }
                        values = [];
                    }
                    value = '';
                }
            }

            svg.split('').forEach((c, ndx) => {
                if ((c >= '0' && c <= '9') || c === '.') {
                    value += c;
                } else if (c === '-') {
                    addValue();
                    value = '-';
                } else if (c === 'm') {
                    addValue();
                    keepNext = true;
                    need = 1;
                    delta = true;
                    mode = 'm';
                } else if (c === 'c') {
                    addValue();
                    keepNext = true;
                    need = 3;
                    delta = true;
                    mode = 'c';
                } else if (c === 'l') {
                    addValue();
                    keepNext = true;
                    need = 1;
                    delta = false;
                    mode = 'l';
                } else if (c === 'M') {
                    addValue();
                    keepNext = true;
                    need = 1;
                    delta = false;
                    mode = 'm';
                } else if (c === 'C') {
                    addValue();
                    keepNext = true;
                    need = 3;
                    delta = false;
                    mode = 'c';
                } else if (c === 'L') {
                    addValue();
                    keepNext = true;
                    need = 1;
                    delta = false;
                    mode = 'l';
                } else if (c === 'Z') {
                    // close the loop
                } else if (c === ',') {
                    addValue();
                } else if (c === ' ') {
                    addValue();
                } else {
                    debugger;  // eslint-disable-line
                }
            });
            addValue();
            let min = points[0].slice();
            let max = points[0].slice();
            for (let i = 1; i < points.length; ++i) {
                min = v2.min(min, points[i]);
                max = v2.max(max, points[i]);
            }
            let range = v2.sub(max, min);
            let halfRange = v2.mult(range, .5);
            for (let i = 0; i < points.length; ++i) {
                const p = points[i];
                if (opt.xFlip) {
                    p[0] = max[0] - p[0];
                } else {
                    p[0] = p[0] - min[0];
                }
                p[1] = (p[1] - min[0]) - halfRange[1];
            }
            return points;
        }

        function getPointOnBezierCurve(points, offset, t) {
            const invT = (1 - t);
            return v2.add(v2.mult(points[offset + 0], invT * invT * invT),
                v2.mult(points[offset + 1], 3 * t * invT * invT),
                v2.mult(points[offset + 2], 3 * invT * t * t),
                v2.mult(points[offset + 3], t * t * t));
        }

        function getPointsOnBezierCurve(points, offset, numPoints) {
            const cpoints = [];
            for (let i = 0; i < numPoints; ++i) {
                const t = i / (numPoints - 1);
                cpoints.push(getPointOnBezierCurve(points, offset, t));
            }
            return cpoints;
        }

        function flatness(points, offset) {
            const p1 = points[offset + 0];
            const p2 = points[offset + 1];
            const p3 = points[offset + 2];
            const p4 = points[offset + 3];

            let ux = 3 * p2[0] - 2 * p1[0] - p4[0]; ux *= ux;
            let uy = 3 * p2[1] - 2 * p1[1] - p4[1]; uy *= uy;
            let vx = 3 * p3[0] - 2 * p4[0] - p1[0]; vx *= vx;
            let vy = 3 * p3[1] - 2 * p4[1] - p1[1]; vy *= vy;

            if (ux < vx) {
                ux = vx;
            }

            if (uy < vy) {
                uy = vy;
            }

            return ux + uy;
        }

        function getPointsOnBezierCurveWithSplitting(points, offset, tolerance, newPoints) {
            const outPoints = newPoints || [];
            if (flatness(points, offset) < tolerance) {

                // just add the end points of this curve
                outPoints.push(points[offset + 0]);
                outPoints.push(points[offset + 3]);

            } else {

                // subdivide
                const t = .5;
                const p1 = points[offset + 0];
                const p2 = points[offset + 1];
                const p3 = points[offset + 2];
                const p4 = points[offset + 3];

                const q1 = v2.lerp(p1, p2, t);
                const q2 = v2.lerp(p2, p3, t);
                const q3 = v2.lerp(p3, p4, t);

                const r1 = v2.lerp(q1, q2, t);
                const r2 = v2.lerp(q2, q3, t);

                const red = v2.lerp(r1, r2, t);

                // do 1st half
                getPointsOnBezierCurveWithSplitting([p1, q1, r1, red], 0, tolerance, outPoints);
                // do 2nd half
                getPointsOnBezierCurveWithSplitting([red, r2, q3, p4], 0, tolerance, outPoints);

            }
            return outPoints;
        }

        // gets points across all segments
        function getPointsOnBezierCurves(points, tolerance) {
            const newPoints = [];
            const numSegments = (points.length - 1) / 3;
            for (let i = 0; i < numSegments; ++i) {
                const offset = i * 3;
                getPointsOnBezierCurveWithSplitting(points, offset, tolerance, newPoints);
            }
            return newPoints;
        }

        // Ramer Douglas Peucker algorithm
        function simplifyPoints(points, start, end, epsilon, newPoints) {
            const outPoints = newPoints || [];

            // find the most distant point from the line formed by the endpoints
            const s = points[start];
            const e = points[end - 1];
            let maxDistSq = 0;
            let maxNdx = 1;
            for (let i = start + 1; i < end - 1; ++i) {
                const distSq = v2.distanceToSegmentSq(points[i], s, e);
                if (distSq > maxDistSq) {
                    maxDistSq = distSq;
                    maxNdx = i;
                }
            }

            // if that point is too far
            if (Math.sqrt(maxDistSq) > epsilon) {

                // split
                simplifyPoints(points, start, maxNdx + 1, epsilon, outPoints);
                simplifyPoints(points, maxNdx, end, epsilon, outPoints);

            } else {

                // add the 2 end points
                outPoints.push(s, e);
            }

            return outPoints;
        }

        // rotates around Y axis.
        function lathePoints(points,
            startAngle,   // angle to start at (ie 0)
            endAngle,     // angle to end at (ie Math.PI * 2)
            numDivisions, // how many quads to make around
            capStart,     // true to cap the top
            capEnd) {     // true to cap the bottom
            const positions = [];
            const texcoords = [];
            const indices = [];

            const vOffset = capStart ? 1 : 0;
            const pointsPerColumn = points.length + vOffset + (capEnd ? 1 : 0);
            const quadsDown = pointsPerColumn - 1;

            // generate v coordniates
            let vcoords = [];

            // first compute the length of the points
            let length = 0;
            for (let i = 0; i < points.length - 1; ++i) {
                vcoords.push(length);
                length += v2.distance(points[i], points[i + 1]);
            }
            vcoords.push(length);  // the last point

            // now divide each by the total length;
            vcoords = vcoords.map(v => v / length);

            // generate points
            for (let division = 0; division <= numDivisions; ++division) {
                const u = division / numDivisions;
                const angle = lerp(startAngle, endAngle, u) % (Math.PI * 2);
                const mat = m4.yRotation(angle);
                if (capStart) {
                    // add point on Y access at start
                    positions.push(0, points[0][1], 0);
                    texcoords.push(u, 0);
                }
                points.forEach((p, ndx) => {
                    const tp = m4.transformPoint(mat, [...p, 0]);
                    positions.push(tp[0], tp[1], tp[2]);
                    texcoords.push(u, vcoords[ndx]);
                });
                if (capEnd) {
                    // add point on Y access at end
                    positions.push(0, points[points.length - 1][1], 0);
                    texcoords.push(u, 1);
                }
            }

            // generate indices
            for (let division = 0; division < numDivisions; ++division) {
                const column1Offset = division * pointsPerColumn;
                const column2Offset = column1Offset + pointsPerColumn;
                for (let quad = 0; quad < quadsDown; ++quad) {
                    indices.push(column1Offset + quad, column1Offset + quad + 1, column2Offset + quad);
                    indices.push(column1Offset + quad + 1, column2Offset + quad + 1, column2Offset + quad);
                }
            }

            return {
                position: positions,
                texcoord: texcoords,
                indices: indices,
            };
        }

        function makeIndexedIndicesFn(arrays) {
            const indices = arrays.indices;
            let ndx = 0;
            const fn = function () {
                return indices[ndx++];
            };
            fn.reset = function () {
                ndx = 0;
            };
            fn.numElements = indices.length;
            return fn;
        }

        function makeUnindexedIndicesFn(arrays) {
            let ndx = 0;
            const fn = function () {
                return ndx++;
            };
            fn.reset = function () {
                ndx = 0;
            };
            fn.numElements = arrays.positions.length / 3;
            return fn;
        }

        function makeIndiceIterator(arrays) {
            return arrays.indices
                ? makeIndexedIndicesFn(arrays)
                : makeUnindexedIndicesFn(arrays);
        }

        function generateNormals(arrays, maxAngle) {
            const positions = arrays.position;
            const texcoords = arrays.texcoord;

            // 首先计算出每个面的法向量
            let getNextIndex = makeIndiceIterator(arrays);
            const numFaceVerts = getNextIndex.numElements;
            const numVerts = arrays.position.length;
            const numFaces = numFaceVerts / 3;
            const faceNormals = [];

            // 计算每个面的法向量，
            // 计算过程中为每个面新建顶点
            for (let i = 0; i < numFaces; ++i) {
                const n1 = getNextIndex() * 3;
                const n2 = getNextIndex() * 3;
                const n3 = getNextIndex() * 3;

                const v1 = positions.slice(n1, n1 + 3);
                const v2 = positions.slice(n2, n2 + 3);
                const v3 = positions.slice(n3, n3 + 3);

                faceNormals.push(m4.normalize(m4.cross(m4.subtractVectors(v1, v2), m4.subtractVectors(v3, v2))));
            }

            let tempVerts = {};
            let tempVertNdx = 0;

            // 假设顶点位置精确匹配

            function getVertIndex(x, y, z) {

                const vertId = x + "," + y + "," + z;
                const ndx = tempVerts[vertId];
                if (ndx !== undefined) {
                    return ndx;
                }
                const newNdx = tempVertNdx++;
                tempVerts[vertId] = newNdx;
                return newNdx;
            }

            // 我们需要算出共享的顶点
            // 这并不像我们看着面那么简单 (三角形)
            // 因为加入我们有一个标准的圆柱
            //
            //
            //      3-4
            //     /   \
            //    2     5   从上往下看，从 S 走到 E, E 和 S 
            //    1     6   是不同的点，因为它们不共享UV坐标。
            //     \   /    
            //      S/E
            //
            // 顶点在其实和结束位置并不是共享的
            // 由于它们有不同的UV坐标，但如果不
            // 把它们看作共享顶点就会得到错误结果

            const vertIndices = [];
            for (let i = 0; i < numVerts; ++i) {
                const offset = i * 3;
                const vert = positions.slice(offset, offset + 3);
                vertIndices.push(getVertIndex(vert));
            }

            // 遍历所有顶点记录所在的面
            const vertFaces = [];
            getNextIndex.reset();
            for (let i = 0; i < numFaces; ++i) {
                for (let j = 0; j < 3; ++j) {
                    const ndx = getNextIndex();
                    const sharedNdx = vertIndices[ndx];
                    let faces = vertFaces[sharedNdx];
                    if (!faces) {
                        faces = [];
                        vertFaces[sharedNdx] = faces;
                    }
                    faces.push(i);
                }
            }

            // 遍历面上的顶点计算每个顶点的法向量
            // 只计算两面角度不大于 maxAngle 面
            // 将结果写入 newPositions,
            // newTexcoords 和 newNormals, 
            // 丢弃相同的顶点
            tempVerts = {};
            tempVertNdx = 0;
            const newPositions = [];
            const newTexcoords = [];
            const newNormals = [];

            function getNewVertIndex(x, y, z, nx, ny, nz, u, v) {
                const vertId =
                    x + "," + y + "," + z + "," +
                    nx + "," + ny + "," + nz + "," +
                    u + "," + v;

                const ndx = tempVerts[vertId];
                if (ndx !== undefined) {
                    return ndx;
                }
                const newNdx = tempVertNdx++;
                tempVerts[vertId] = newNdx;
                newPositions.push(x, y, z);
                newNormals.push(nx, ny, nz);
                newTexcoords.push(u, v);
                return newNdx;
            }

            const newVertIndices = [];
            getNextIndex.reset();
            const maxAngleCos = Math.cos(maxAngle);
            // 对每个面
            for (let i = 0; i < numFaces; ++i) {
                const thisFaceVertexNormals = [];
                // 获取该面的法向量
                const thisFaceNormal = faceNormals[i];
                // 对于面上的每一点
                for (let j = 0; j < 3; ++j) {
                    const ndx = getNextIndex();
                    const sharedNdx = vertIndices[ndx];
                    const faces = vertFaces[sharedNdx];
                    const norm = [0, 0, 0];
                    faces.forEach(faceNdx => {
                        // 面的法向量是否相同
                        const otherFaceNormal = faceNormals[faceNdx];
                        const dot = m4.dot(thisFaceNormal, otherFaceNormal);
                        if (dot > maxAngleCos) {
                            m4.addVectors(norm, otherFaceNormal, norm);
                        }
                    });
                    m4.normalize(norm, norm);
                    const poffset = ndx * 3;
                    const toffset = ndx * 2;
                    newVertIndices.push(getNewVertIndex(
                        positions[poffset + 0], positions[poffset + 1], positions[poffset + 2],
                        norm[0], norm[1], norm[2],
                        texcoords[toffset + 0], texcoords[toffset + 1]));
                }
            }

            return {
                position: newPositions,
                texcoord: newTexcoords,
                normal: newNormals,
                indices: newVertIndices,
            };

        }

        webglLessonsUI.setupUI(document.querySelector("#ui"), data, [
            { type: "slider", key: "distance", change: update, min: 0.001, max: 5, precision: 3, step: 0.001, },
            { type: "slider", key: "divisions", change: update, min: 1, max: 60, },
            { type: "slider", key: "startAngle", change: update, min: 0, max: Math.PI * 2, precision: 3, step: 0.001, uiMult: 180 / Math.PI, uiPrecision: 0 },
            { type: "slider", key: "endAngle", change: update, min: 0, max: Math.PI * 2, precision: 3, step: 0.001, uiMult: 180 / Math.PI, uiPrecision: 0 },
            { type: "slider", key: "maxAngle", change: update, min: 0.001, max: Math.PI, precision: 3, step: 0.001, uiMult: 180 / Math.PI, uiPrecision: 1 },
            { type: "checkbox", key: "capStart", change: update },
            { type: "checkbox", key: "capEnd", change: update },
            { type: "checkbox", key: "triangles", change: render },
            { type: "option", key: "mode", change: render, options: ["normals", "lit", "texcoords"], },
        ]);

        /* eslint brace-style: 0 */
        gl.canvas.addEventListener('mousedown', (e) => { e.preventDefault(); startRotateCamera(e); });
        window.addEventListener('mouseup', stopRotateCamera);
        window.addEventListener('mousemove', rotateCamera);
        gl.canvas.addEventListener('touchstart', (e) => { e.preventDefault(); startRotateCamera(e.touches[0]); });
        window.addEventListener('touchend', (e) => { stopRotateCamera(e.touches[0]); });
        window.addEventListener('touchmove', (e) => { rotateCamera(e.touches[0]); });

        let lastPos;
        let moving;
        function startRotateCamera(e) {
            lastPos = getRelativeMousePosition(gl.canvas, e);
            moving = true;
        }

        function rotateCamera(e) {
            if (moving) {
                const pos = getRelativeMousePosition(gl.canvas, e);
                const size = [4 / gl.canvas.width, 4 / gl.canvas.height];
                const delta = v2.mult(v2.sub(lastPos, pos), size);

                // this is bad but it works for a basic case so phffttt
                worldMatrix = m4.multiply(m4.xRotation(delta[1] * 5), worldMatrix);
                worldMatrix = m4.multiply(m4.yRotation(delta[0] * 5), worldMatrix);

                lastPos = pos;

                render();
            }
        }

        function stopRotateCamera(e) {
            moving = false;
        }

        function degToRad(deg) {
            return deg * Math.PI / 180;
        }

        function clamp(v, min, max) {
            return Math.max(Math.min(v, max), min);
        }

        function lerp(a, b, t) {
            return a + (b - a) * t;
        }

        function getQueryParams() {
            var params = {};
            if (window.location.search) {
                window.location.search.substring(1).split("&").forEach(function (pair) {
                    var keyValue = pair.split("=").map(function (kv) {
                        return decodeURIComponent(kv);
                    });
                    params[keyValue[0]] = keyValue[1];
                });
            }
            return params;
        }

        function getRelativeMousePosition(canvas, e) {
            const rect = canvas.getBoundingClientRect();
            const x = (e.clientX - rect.left) / (rect.right - rect.left) * canvas.width;
            const y = (e.clientY - rect.top) / (rect.bottom - rect.top) * canvas.height;
            return [
                (x - canvas.width / 2) / window.devicePixelRatio,
                (y - canvas.height / 2) / window.devicePixelRatio,
            ];
        }

        // creates a texture info { width: w, height: h, texture: tex }
        // The texture will start with 1x1 pixels and be updated
        // when the image has loaded
        function loadImageAndCreateTextureInfo(url, callback) {
            var tex = gl.createTexture();
            gl.bindTexture(gl.TEXTURE_2D, tex);
            // Fill the texture with a 1x1 blue pixel.
            gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE,
                new Uint8Array([0, 0, 255, 255]));

            var textureInfo = {
                width: 1,   // we don't know the size until it loads
                height: 1,
                texture: tex,
            };
            var img = new Image();
            img.addEventListener('load', function () {
                textureInfo.width = img.width;
                textureInfo.height = img.height;

                gl.bindTexture(gl.TEXTURE_2D, textureInfo.texture);
                gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, img);

                // Check if the image is a power of 2 in both dimensions.
                if (isPowerOf2(img.width) && isPowerOf2(img.height)) {
                    // Yes, it's a power of 2. Generate mips.
                    gl.generateMipmap(gl.TEXTURE_2D);
                } else {
                    // No, it's not a power of 2. Turn of mips and set wrapping to clamp to edge
                    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
                    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
                    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
                }

                if (callback) {
                    callback();
                }
            });
            img.src = url;

            return textureInfo;
        }

        function isPowerOf2(value) {
            return (value & (value - 1)) === 0;
        }
    }

    const v2 = (function () {
        // adds 1 or more v2s
        function add(a, ...args) {
            const n = a.slice();
            [...args].forEach(p => {
                n[0] += p[0];
                n[1] += p[1];
            });
            return n;
        }

        function sub(a, ...args) {
            const n = a.slice();
            [...args].forEach(p => {
                n[0] -= p[0];
                n[1] -= p[1];
            });
            return n;
        }

        function mult(a, s) {
            if (Array.isArray(s)) {
                let t = s;
                s = a;
                a = t;
            }
            if (Array.isArray(s)) {
                return [
                    a[0] * s[0],
                    a[1] * s[1],
                ];
            } else {
                return [a[0] * s, a[1] * s];
            }
        }

        function lerp(a, b, t) {
            return [
                a[0] + (b[0] - a[0]) * t,
                a[1] + (b[1] - a[1]) * t,
            ];
        }

        function min(a, b) {
            return [
                Math.min(a[0], b[0]),
                Math.min(a[1], b[1]),
            ];
        }

        function max(a, b) {
            return [
                Math.max(a[0], b[0]),
                Math.max(a[1], b[1]),
            ];
        }

        // compute the distance squared between a and b
        function distanceSq(a, b) {
            const dx = a[0] - b[0];
            const dy = a[1] - b[1];
            return dx * dx + dy * dy;
        }

        // compute the distance between a and b
        function distance(a, b) {
            return Math.sqrt(distanceSq(a, b));
        }

        // compute the distance squared from p to the line segment
        // formed by v and w
        function distanceToSegmentSq(p, v, w) {
            const l2 = distanceSq(v, w);
            if (l2 === 0) {
                return distanceSq(p, v);
            }
            let t = ((p[0] - v[0]) * (w[0] - v[0]) + (p[1] - v[1]) * (w[1] - v[1])) / l2;
            t = Math.max(0, Math.min(1, t));
            return distanceSq(p, lerp(v, w, t));
        }

        // compute the distance from p to the line segment
        // formed by v and w
        function distanceToSegment(p, v, w) {
            return Math.sqrt(distanceToSegmentSq(p, v, w));
        }

        return {
            add: add,
            sub: sub,
            max: max,
            min: min,
            mult: mult,
            lerp: lerp,
            distance: distance,
            distanceSq: distanceSq,
            distanceToSegment: distanceToSegment,
            distanceToSegmentSq: distanceToSegmentSq,
        };
    }());

    main();
</script>

</html>